#pragma once

#include <vex/iterable/range_base.h>
#include <vex/iterable/adapter/adapter_default.h>
#include <vex/iterable/adapter/adapter_access.h>
#include <type_traits>
#include <utility>

namespace vex { namespace query {

    /// Some commonly used selectors which are clumsy to write even with lambda syntax
    namespace selectors {

        namespace adl_get {
            struct unused_type;
            /// introduce a fake get to enable ADL
            template<int Index> void get(unused_type&&);
        }

        using adl_get::get;

        /// <summary>
        /// Extracts the <paramref name="Index"/>-th element from a tuple like argument. 
        /// Supports std::pair, std::tuple or boost::tuple out of the box via <c>get</c> protocol
        /// </summary>
        template<int Index> 
        class get_ith {
            template<int> struct index;

            template<> struct index<0u> {
                template<class TPair>
                auto operator()(TPair& p_pair) const -> decltype((p_pair.first))
                {
                    return p_pair.first;
                }
            };

            template<> struct index<1u> {
                template<class TPair>
                auto operator()(TPair& p_pair) const -> decltype((p_pair.second))
                {
                    return p_pair.second;
                }
            };
        public:
            template<class TTuple>
            auto operator()(TTuple&& p_tuple) const
                -> decltype(get<Index>(std::forward<TTuple>(p_tuple)))
            {
                return get<Index>(std::forward<TTuple>(p_tuple));
            }

            template<class T1, class T2>
            auto operator()(std::pair<T1, T2> const& p_tuple, T1* = 0) const
                -> decltype(index<Index>()(p_tuple))
            {
                return index<Index>()(p_tuple);
            }

            template<class T1, class T2>
            auto operator()(std::pair<T1, T2>& p_tuple, T1* = 0, T2* = 0) const
                -> decltype(index<Index>()(p_tuple))
            {
                return index<Index>()(p_tuple);
            }
        };

        /// <summary>
        /// Converts the result of applying the dereference operator (*) to argument
        /// </summary>
        struct dereference {
            template<class T> 
            auto operator()(T const& p_element) const
                -> decltype(*p_element)
            {
                return *p_element;
            }
        };

        /// <summary>
        /// Converts an reference type to referenced value type
        /// </summary>
        struct remove_reference {
            template<class T> 
            auto operator()(T const& p_value) const -> T
            {
                return p_value;
            }
        };
    }

    namespace adapter {

        namespace detail {
            template<class TAdaptee, class TSelector, class TRef> 
            class select_adapter_body : public iterable::adapter::adapter_default<
                select_adapter_body<TAdaptee, TSelector, TRef>,
                typename TAdaptee::category,
                TRef
            >
            {
                friend class iterable::adapter::adapter_access;

                template<class, class, class> friend class select_adapter_body;

                typedef TAdaptee adaptee_type;
                typedef TSelector selector_type;

                adaptee_type m_adaptee;
                mutable selector_type m_selector;

                adaptee_type& get_adaptee_ref() { return m_adaptee; }
                adaptee_type const& get_adaptee_ref() const { return m_adaptee; }
            public:
                template<class UAdaptee, class USelector>
                select_adapter_body(UAdaptee p_adaptee, USelector p_selector)
                    : m_adaptee(p_adaptee)
                    , m_selector(p_selector) {}

                select_adapter_body(select_adapter_body&& p_other)
                    : m_adaptee(std::move(p_other.m_adaptee))
                    , m_selector(std::move(p_other.m_selector)) {}

                TRef front() const
                {
                    return m_selector(m_adaptee.front());
                }

                TRef back() const
                {
                    return m_selector(m_adaptee.back());
                }

                TRef at(std::size_t p_index) const
                {
                    return m_selector(m_adaptee.at(p_index));
                }

                select_adapter_body save() const
                {
                    return select_adapter_body(m_adaptee.save(), m_selector);
                }
            };
        }

        template<class TAdaptee, class TSelector> struct select_adapter_traits {
            typedef typename std::decay<TAdaptee>::type adaptee_type;
            typedef typename std::decay<TSelector>::type selector_type;

            typedef iterable::range_traits<TAdaptee> adaptee_traits;

            // reference type is determined by the selector
            typedef decltype(std::declval<selector_type>()(std::declval<typename adaptee_traits::reference>())) reference;

            // resulting category is unchanged
            typedef typename adaptee_traits::category category;

            typedef detail::select_adapter_body<
                adaptee_type, 
                selector_type,
                reference
            > adapter_body_type;
        };

        template<class TAdaptee, class TSelector>
        class select_adapter : public iterable::range_base<  
            typename select_adapter_traits<TAdaptee, TSelector>::category,
            typename select_adapter_traits<TAdaptee, TSelector>::reference,
            iterable::adapter::adapter_default<
                select_adapter<TAdaptee, TSelector>,
                typename select_adapter_traits<TAdaptee, TSelector>::category,
                typename select_adapter_traits<TAdaptee, TSelector>::reference
            >
        >
        {
            typedef typename select_adapter_traits<TAdaptee, TSelector>::adapter_body_type adaptee_type;

            friend class iterable::adapter::adapter_access;

            template<class, class> friend class select_adapter;

            adaptee_type m_adapter_body;
            adaptee_type& get_adaptee_ref() { return m_adapter_body; }
            adaptee_type const & get_adaptee_ref() const { return m_adapter_body; }

            select_adapter(adaptee_type p_select_adapter_body)
                : m_adapter_body(p_select_adapter_body) {}
        public:
            select_adapter(
                TAdaptee p_adaptee, 
                TSelector p_selector)
                : m_adapter_body(p_adaptee, p_selector) {}

            select_adapter(select_adapter&& p_other)
                : m_adapter_body(std::move(p_other.m_adapter_body)) {}

            template<class UAdaptee, class USelector>
            select_adapter(select_adapter<UAdaptee, USelector> const & p_other, typename std::enable_if<
                std::is_convertible<UAdaptee, TAdaptee>::value &&
                std::is_convertible<USelector, TSelector>::value
            >::type* = 0) : m_adapter_body(p_other.m_adapter_body) {}
        };

        namespace detail {
            template<class TSelector> struct select_dispatch {
                typedef typename std::decay<TSelector>::type selector_type;
                TSelector m_selector;

                explicit select_dispatch(TSelector p_selector)
                    : m_selector(p_selector) {}
            };

            template<class TRange, class TSelector> 
            auto operator | (TRange&& p_range, select_dispatch<TSelector> const & p_dispatch) 
                -> adapter::select_adapter<typename std::decay<TRange>::type, typename std::decay<TSelector>::type>
            {
                return adapter::select_adapter<
                    typename std::decay<TRange>::type, 
                    typename std::decay<TSelector>::type
                >(std::forward<TRange>(p_range), p_dispatch.m_selector);
            }
        }
    }

    template<class TSelector> auto select(TSelector p_selector)
        -> adapter::detail::select_dispatch<TSelector>
    {
        return adapter::detail::select_dispatch<TSelector>(p_selector);
    }

}}